package doj;

import robocode.HitByBulletEvent;
import robocode.Robot;
import robocode.ScannedRobotEvent;
/**
 * Competitive robot.
 * @author Jordan Do
 *
 */
public class DojBot extends Robot {
  protected Boolean tooClose = null; //Used if enemy is too close to try to dodge.
  protected EnemyRobot enemy = null;//Robot will only pick one target
  protected double oldEnergy = 100;//Energy of other robot for dodging.
  protected double oldHeading = 0;//Heading of robot to see if it's turning
  protected long scanTime;
  
  /**
   * Main method.
   */
  public void run() {
    //allow gun and radar to turn independently of body
    setAdjustGunForRobotTurn(true);
    setAdjustRadarForRobotTurn(true);
    setAdjustRadarForGunTurn(true);
    while (true) {
        turnRadarRight(360);
    }
  }
  /**
   * Moves perpendicular to bullet if hit and not too close to a robot.
   * @param bullet HitByBulletEvent
   */
  public void onHitByBullet(HitByBulletEvent bullet) {
    if (!tooClose) {
      if (bullet.getBearing() + 90 > 180) {
        turnRight(bullet.getBearing() - 90);
      }
      else {
        turnRight(bullet.getBearing() + 90);
      }
      ahead(35);
    }
  }
  /**
   * When it finds a scanned robot.
   * @param event robot taken by the scanner
   */
  public void onScannedRobot(ScannedRobotEvent event) {
    //if a robot hasn't been seen yet, choose this one to follow
    if (enemy == null) {
      enemy = new EnemyRobot();
      enemy.update(event);
      setCoordinates(enemy);
    }
    if (event.getName().equals(enemy.getName())) {
      enemy.update(event);
      setCoordinates(enemy);
      scanTime = getTime();
      predict(enemy);
      Double change = new Double(getHeading() + enemy.getBearing());
      if (change < 0) {
        change += 360;
      }
      if (change >= 360) {
        change -= 360;
      }
      double turnAmount = change - getRadarHeading();
      turnRadarRight(shorten(turnAmount));
      turnAmount = change - getGunHeading();
      turnGunRight(shorten(turnAmount));
      closeCheck(enemy);
      scan();
    }
  }
  
  /**
   * Determines the X,Y position of the enemy robot.
   * @param enemy EnemyRobot object
   */
  protected void setCoordinates(EnemyRobot enemy) {    
    double absBearing = getHeading() + enemy.getBearing();
    if (absBearing < 0) {
      absBearing += 360;
    }
    if (absBearing >= 360) {
      absBearing -= 360;
    }
    //guess is often off by 1 turn
    double x = getX() + Math.sin(Math.toRadians(absBearing)) * enemy.getDistance();
    double y = getY() + Math.cos(Math.toRadians(absBearing)) * enemy.getDistance();
    //one tick forward
    y = y + Math.cos(Math.toRadians(enemy.getHeading())) * enemy.getVelocity();
    x = x + Math.sin(Math.toRadians(enemy.getHeading())) * enemy.getVelocity();
    enemy.setY(y);
    enemy.setX(x);
  }
  /**
   * turns right or left, whichever is shorter.
   * @param turnAmount the original amount to turn
   * @return amount to turn shortened
   */
  protected double shorten(double turnAmount) {
    if (turnAmount > 180) {
      return (360 - turnAmount);
    }
    else if (turnAmount < -180) {
      return (360 + turnAmount);
    }
      return turnAmount;
  }
  /**
   * checks if opponent is too close to move.
   * @param enemy scannedrobotevent
   */
  protected void closeCheck(EnemyRobot enemy) {
    if (enemy.getDistance() < 175) {
      tooClose = true;
    }
    else {
      //tries to dodge if it sees a change in energy signifying bullets
      tooClose = false;
      if (oldEnergy - enemy.getEnergy() > 0 && oldEnergy - enemy.getEnergy() <= 3) {
        if (enemy.getBearing() + 90 > 180) {
          turnRight(enemy.getBearing() - 90);
        }
        else {
          turnRight(enemy.getBearing() + 90);
        }
        ahead(35);
        oldEnergy = enemy.getEnergy();
      }
    }
  }

  /**
   * tries to predict enemies movement in linear line.
   * @param enemy ScannedRobotEvent
   */
  protected void predict(EnemyRobot enemy) {
    if (oldHeading == enemy.getHeading()) {
      linearFuture(enemy);
    }
    else {
      circularFuture(enemy);
    }
    oldHeading = enemy.getHeading();
    enemy.setTime(scanTime);
    // calculate gun turn to predicted x,y location
    double absDeg = pointBearing(getX(), getY(), enemy.getFutureX(), enemy.getFutureY());
    // turn the gun to the predicted x,y location
    double angle = absDeg - getGunHeading();
    angle = shorten(angle);
    if (enemy.getFutureY() <= getBattleFieldHeight() && enemy.getFutureX() <= getBattleFieldWidth()
        && enemy.getFutureY() >= 0 && enemy.getFutureX() >= 0 && 
        getGunHeat() == 0) {
      turnGunRight(angle);
      fire(3.0);
    }
  }
  /**
   * Tries to predict circular motion.
   * @param enemy EnemyRobot
   */
  protected void circularFuture(EnemyRobot enemy) {
    out.println("circular targetting");
    double approxMe = 0;
    double headPerTurn = (Math.abs(enemy.getHeading() - oldHeading) 
        / (scanTime - enemy.getScanTime()));
    double radius = enemy.getVelocity() / headPerTurn;
    double guess = 1;
    for (int i = 0; i < 500; i++) {
      guess = i * headPerTurn;
      enemy.setFutureY(enemy.getY() + 
          (Math.sin(Math.toRadians(enemy.getHeading() + guess)) * radius) - 
            (Math.sin(Math.toRadians(enemy.getHeading()) * radius)));
      enemy.setFutureX(enemy.getX() + (
          Math.cos(Math.toRadians(enemy.getHeading()) * radius)) - 
            (Math.cos(Math.toRadians((enemy.getHeading() + guess))) * radius));
      approxMe = pointDistance(getX(), getY(), enemy.getFutureX(), enemy.getFutureY()) / 11;
      //if the two guesses don't match up, modify time and try again
      if (Math.abs(approxMe - i) < 1) {
        out.println(approxMe + " " + i);
        break;
      } 
    }
  }
  /**
   * Predict the enemy's point based on linear movement.
   * @param enemy EnemyRobot object
   */
  protected void linearFuture(EnemyRobot enemy) {
    out.println("linear targetting");
    //guess time to start
    double time = enemy.getDistance() / 11;
    double approxEnemy = 0;
    double approxMe = 0;
    //try to find the x,y that they will intersect at, give up if can't within 1000
    for (int i = 0; i < 1000; i++) {
      enemy.setFutureY(enemy.getY() + Math.cos(Math.toRadians(enemy.getHeading())) 
          * enemy.getVelocity() * time);
      enemy.setFutureX(enemy.getX() + Math.sin(Math.toRadians(enemy.getHeading())) 
          * enemy.getVelocity() * time);
      if (enemy.getFutureX() < 0 || enemy.getFutureY() < 0) {
        break;
      }
      approxEnemy = pointDistance(enemy.getX(), enemy.getY(), 
          enemy.getFutureX(), enemy.getFutureY()) 
          / enemy.getVelocity();
      approxMe = pointDistance(getX(), getY(), enemy.getFutureX(), enemy.getFutureY()) / 11;
      //if the two guesses don't match up, modify time and try again
      if (Math.abs(approxMe - approxEnemy) > 1) {
        if (approxMe > approxEnemy) {
          time += 1;
        }
        else {
          time -= 1;
        }
      }
      else {
        break;
      }
    }
  }
  /**
   * calculates the absolute bearing one point to another.
   * @param x1  x from one point
   * @param y1  y from one point
   * @param x2  x from second point
   * @param y2  y from second point
   * @return  the bearing between the two points
   */
  protected double pointBearing(double x1, double y1, double x2, double y2) {
    double x = x2 - x1;
    if (x == 0) {
      if (y1 > y2) {
        return 180;
      }
      else {
        return 0;
      }
    }
    double y = y2 - y1;
    if (y == 0) {
      if (x1 > x2) {
        return 270;
      }
      else {
        return 90;
      }
    }
    double hypotenuse = Math.sqrt(x * x + y * y);
    double arcSin = Math.toDegrees(Math.asin(x / hypotenuse));
    double bearing = 0;
    //find which quadrant and change how you have to turn
    if (x > 0 && y > 0) {
      bearing = arcSin;
    } 
    else if (x < 0 && y > 0) {
      bearing = 360 + arcSin; 
    } 
    else if (x > 0 && y < 0) {
      bearing = 180 - arcSin;
    } 
    else if (x < 0 && y < 0) {
      bearing = 180 - arcSin;
    }
    return bearing;
  }
  /**
   * Finds the distance between two points.
   * @param x1 point 1 x
   * @param y1 point 1 y
   * @param x2 point 2 x
   * @param y2 point 2 y
   * @return Distance from point 1 to 2
   */
  protected double pointDistance(double x1, double y1, double x2, double y2) {
    if (x1 == x2) {
      return Math.abs(y1 - y2);
    }
    else if (y1 == y2) {
      return Math.abs(x1 - x2);
    }
    else {
      return Math.sqrt( Math.abs(y1 - y2) * Math.abs(y1 - y2) 
          + Math.abs(x1 - x2) * Math.abs(x1 - x2));
    }
  }
}
